using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;

namespace Microsoft.Coding4Fun.TweetCraft.Tray.Support
{
	/// <summary>
	/// Provides support for controlling http based downloads. 
	/// </summary>
	public class DownloadManager : IDownloadManager
	{
		//The maximum number of characters to read from the stream
		const int MaxBufferSize = 1024;
		//The request time-out in milliseconds
		const int DefaultTimeout = 120000;

		private bool disposed;
		private readonly ManualResetEvent manualResetEvent = new ManualResetEvent(false);

		/// <summary>
		/// Downloads the specified file from the given url in asynchronous way.
		/// </summary>
		/// <param name="uri">The URL.</param>
		/// <param name="completedCallback">The completed callback.</param>
		public void DownloadFile(Uri uri, Action<Stream, Exception> completedCallback)
		{
			if (disposed)
				throw new ObjectDisposedException("Resource was disposed.");

			if (uri == null)
				throw new ArgumentNullException("uri");
			if (completedCallback == null)
				throw new ArgumentNullException("completedCallback");

			try
			{
				var request = (HttpWebRequest) CreateWebRequest(uri);
				// Create an instance of the RequestState and assign the previous myHttpWebRequest
				// object to its request field.
				var requestState = new RequestState
				                   	{
				                   		Request = request,
										CompletedCallback = completedCallback
				                   	};
				// Start the asynchronous request.
				IAsyncResult result = request.BeginGetResponse(DownloadFileCallback, requestState);
				// This line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted
				ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, TimeoutCallback, request, DefaultTimeout, true);
				// The response came in the allowed time. The work processing will happen in the callback function.
				manualResetEvent.WaitOne();
				// Release the HttpWebResponse resource.
				if (requestState.Response != null)
					requestState.Response.Close();
				else
					completedCallback.Invoke(null, requestState.HandledException);
			}
			catch (WebException ex)
			{
				Trace.TraceError(String.Format(CultureInfo.CurrentCulture, Strings.CouldNotDownloadFile, uri.AbsoluteUri));
				completedCallback.Invoke(null, ex);
			}
		}

		/// <summary>
		/// Downloads the specified file from the web and return content as string.
		/// </summary>
		/// <param name="uri">The URI.</param>
		/// <param name="completedCallback">The completed callback.</param>
		public void DownloadFileAsString(Uri uri, Action<string, Exception> completedCallback)
		{
			if (disposed)
				throw new ObjectDisposedException("Resource was disposed.");

			if (uri == null)
				throw new ArgumentNullException("uri");
			if (completedCallback == null)
				throw new ArgumentNullException("completedCallback");

			//Call download file
			DownloadFile(uri, (stream, ex) =>
			{
				string resultText = null;
				if (stream != null)
				{
					var buffer = new char[MaxBufferSize];
					if (stream.Position > 0)
						stream.Position = 0;

					var streamReader = new StreamReader(stream, Encoding.UTF8);
					var stringWriter = new StringWriter(CultureInfo.CurrentCulture);
					int bytesRead = streamReader.Read(buffer, 0, MaxBufferSize);
					while (bytesRead != 0)
					{
						stringWriter.Write(buffer, 0, MaxBufferSize);
						bytesRead = streamReader.Read(buffer, 0, MaxBufferSize);
					}
					resultText = stringWriter.ToString();
				}
				completedCallback.Invoke(resultText, ex);
			});
		}

		/// <summary>
		/// Executing when download started.
		/// </summary>
		/// <param name="asyncResult">The async result.</param>
		protected void DownloadFileCallback(IAsyncResult asyncResult)
		{
			// State of request is asynchronous.
			var requestState = (RequestState) asyncResult.AsyncState;
			HttpWebRequest webRequest = requestState.Request;
			try
			{
				requestState.Response = (HttpWebResponse) webRequest.EndGetResponse(asyncResult);
				// Read the response into a Stream object.
				Stream responseStream = requestState.Response.GetResponseStream();
				requestState.StreamResponse = responseStream;

				// Begin the Reading of the contents of the HTML page and print it to the console.
				responseStream.BeginRead(requestState.BufferRead, 0, MaxBufferSize, ReadCallback, requestState);
				return;
			}
			catch (WebException webException)
			{
				Trace.TraceError("{0} Original exception: {1}.",
				                 String.Format(CultureInfo.CurrentCulture, Strings.CouldNotDownloadFile, webRequest.Address.AbsoluteUri), webException.Message);
				requestState.HandledException = webException;
				manualResetEvent.Set();
			}
		}

		/// <summary>
		/// Callback method for reading Response stream internally.
		/// </summary>
		/// <param name="asyncResult">The async result.</param>
		protected void ReadCallback(IAsyncResult asyncResult)
		{
			var requestState = (RequestState)asyncResult.AsyncState;
			try
			{
				Stream responseStream = requestState.StreamResponse;
				int read = responseStream.EndRead(asyncResult);

				if (read > 0)
				{
					requestState.RequestData.Write(requestState.BufferRead, 0, read);
					responseStream.BeginRead(requestState.BufferRead, 0, MaxBufferSize, ReadCallback, requestState);
					return;
				}
				
				if (requestState.CompletedCallback != null)
				{
					requestState.CompletedCallback.Invoke(requestState.RequestData, null);
				}

				responseStream.Close();
			}
			catch (WebException e)
			{
				Trace.TraceError("{0} Original exception: {1}.", String.Format(CultureInfo.CurrentCulture, Strings.CouldNotDownloadFile, requestState.Request.Address.AbsoluteUri), e.Message);
			}
			manualResetEvent.Set();
		}

		/// <summary>
		/// Abort the request if the timer fires.
		/// </summary>
		/// <param name="state">The state.</param>
		/// <param name="timedOut">if set to <c>true</c> [timed out].</param>
		private static void TimeoutCallback(object state, bool timedOut)
		{
			if (timedOut)
			{
				var request = state as HttpWebRequest;
				if (request != null)
				{
					request.Abort();
				}
			}
		}

		/// <summary>
		/// Creates the web request.
		/// </summary>
		/// <param name="uri">The URI.</param>
		/// <returns></returns>
		protected virtual WebRequest CreateWebRequest(Uri uri)
		{
			if (uri == null)
				throw new ArgumentNullException("uri");
			return WebRequest.Create(uri);
		}

		/// <summary>
		/// This class stores the State of the request.
		/// </summary>
		private class RequestState
		{
			/// <summary>
			/// Initializes a new instance of the <see cref="RequestState"/> class.
			/// </summary>
			public RequestState()
			{
				BufferRead = new byte[MaxBufferSize];
				RequestData = new MemoryStream();
				Request = null;
				StreamResponse = null;
			}

			/// <summary>
			/// Gets or sets the request data.
			/// </summary>
			/// <value>The request data.</value>
			public MemoryStream RequestData { get; private set; }

			/// <summary>
			/// Gets or sets the buffer read.
			/// </summary>
			/// <value>The buffer read.</value>
			public byte[] BufferRead { get; private set; }

			/// <summary>
			/// Gets or sets the request.
			/// </summary>
			/// <value>The request.</value>
			public HttpWebRequest Request { get; set; }

			/// <summary>
			/// Gets or sets the response.
			/// </summary>
			/// <value>The response.</value>
			public HttpWebResponse Response { get; set; }

			/// <summary>
			/// Gets or sets the stream response.
			/// </summary>
			/// <value>The stream response.</value>
			public Stream StreamResponse { get; set; }

			/// <summary>
			/// Gets or sets the completed callback.
			/// </summary>
			/// <value>The completed callback.</value>
			public Action<Stream, Exception> CompletedCallback { get; set; }

			/// <summary>
			/// Gets or sets the handled exception.
			/// </summary>
			/// <value>The handled exception.</value>
			public Exception HandledException { get; set; }

		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if (!disposed)
			{
				if (disposing)
				{
					manualResetEvent.Close();
				}
				disposed = true;
			}
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}
	}
}